﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
#if NETFX_CORE
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
#else
using System.Windows;
#if SILVERLIGHT
using System.Windows.Controls;
#else
using System.Windows.Media;
#endif
using System.Windows.Input;
#endif

namespace FreshMeat.Xaml
{
   /// <summary>
   /// Provides methods for dealing with focus in a common way for both WPF and Silverlight.
   /// </summary>
   /// <remarks>
   /// <para>
   /// For Silverlight, Control is the least derived type which can have the focus.  For
   /// WPF, UIElement is treated as the equivalent type even though technically it would
   /// be IInputElement.  By choosing UIElement instead, we can make certain common 
   /// assumptions, such as both UIElement and Control both having a Dispatcher.
   /// </para>
   /// <para>
   /// WPF has a much richer focus model, dealing with both logical and keyboard focus
   /// while Silverlight has a much simpler model where the two are one and the same.
   /// This helper uses the Silverlight model for both platforms.  When working with
   /// WPF, focus means keyboard focus, and logical focus and logical focus scopes
   /// are ignored.
   /// </para>
   /// </remarks>
   public static class FocusHelper
   {
      static readonly BooleanSwitch trace = new BooleanSwitch("Focus", "Focus");
      static readonly List<FocusContext> contexts = new List<FocusContext>();

#if !SILVERLIGHT && !NETFX_CORE
      /// <summary>
      /// Finds the nearest focus scope to which the element belongs.
      /// The element itself can be return if it is a focus scope.
      /// </summary>
      /// <param name="element"></param>
      /// <returns>The focus scope or null if there is none.</returns>
      public static DependencyObject FindParentFocusScope(DependencyObject element)
      {
         if (FocusManager.GetIsFocusScope(element))
            return element;

         var parent = VisualTreeHelper.GetParent(element);
         return parent != null ? FindParentFocusScope(parent) : null;
      }
#endif

      /// <summary>
      /// Finds the first element that meets the conditions of <see cref="IsFocusable"/>.
      /// </summary>
      /// <param name="startingObject">The starting object for the search.</param>
#if !SILVERLIGHT && !NETFX_CORE
      public static UIElement FindFirstFocusableElement(DependencyObject startingObject)
      {
         return VisualTreeEnumerator.EnumerateDepthFirst(startingObject).OfType<UIElement>().Where(IsFocusable).FirstOrDefault();
      }
#else
      public static Control FindFirstFocusableElement(Control startingObject)
      {
         return VisualTreeEnumerator.EnumerateDepthFirst(startingObject).OfType<Control>().Where(IsFocusable).FirstOrDefault();
      }
#endif

      /// <summary>
      /// Indicates whether the object can accept the focus.
      /// </summary>
      public static bool IsFocusable(DependencyObject o)
      {
#if !SILVERLIGHT && !NETFX_CORE
         var uiElement = o as UIElement;
         if (uiElement == null)
            return false;

         if (!uiElement.Focusable)
            return false;

         if (!uiElement.IsEnabled)
            return false;

         var frameworkElement = o as FrameworkElement;
         if (frameworkElement != null && !frameworkElement.IsVisible)
            return false;
#else
         var control = o as Control;
         if (control == null)
            return false;

         if (control.Visibility != Visibility.Visible)
            return false;

         if (!control.IsTabStop)
            return false;

         if (!control.IsEnabled)
            return false;
#endif

         // Ignore elements that contain other focusable elements
         //if (FindFirstFocusableElement(inputElement) != null)
         //   return false;

         return true;
      }

      /// <summary>
      /// Moves focus to the next object.
      /// </summary>
      /// <param name="element"></param>
      public static void MoveFocusNext(FrameworkElement element)
      {
#if !SILVERLIGHT && !NETFX_CORE
         element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
#else
         // TODO: No idea how to move focus to the next focusable item in Silverlight.
#endif
      }

      /// <summary>
      /// Determines whether the focus is within an element (meaning either the element
      /// or one of its children has the focus).
      /// </summary>
      public static bool IsFocusWithin(FrameworkElement element)
      {
#if !SILVERLIGHT && !NETFX_CORE
         return element.IsKeyboardFocusWithin;
#else
         // TODO: Get current focused element and check if the specified element is an ancestor.
         return true;
#endif
      }
     
      /// <summary>
      /// Attempts to set the focus to the specified object.
      /// </summary>
      /// <param name="target">The object which is to receive the focus.</param>
      /// <returns>True if the focus was set to the object, false otherwise.</returns>
#if !SILVERLIGHT && !NETFX_CORE
      public static bool SetFocus(UIElement target)
#else
      public static bool SetFocus(Control target)
#endif
      {
         var worked = false;

         if (contexts.Count == 0)
         {
#if NETFX_CORE
            worked = target.Focus(FocusState.Programmatic);
#elif !SILVERLIGHT
            worked = Keyboard.Focus(target) == target;
#else
            worked = target.Focus();
#endif
            if (trace.Enabled)
               Trace.TraceInformation(string.Format("FocusHelper.SetFocus \"{0}\" ({1})", XamlHelper.GetPath(target), worked));
         }
         else
         {
            if (trace.Enabled)
               Trace.TraceInformation(string.Format("FocusHelper.SetFocus ignoring call due to temp context (would have been \"{0}\")", XamlHelper.GetPath(target)));
         }

         return worked;
      }

      /// <summary>
      /// Attempts to temporarily set focus to the specified object.
      /// </summary>
      /// <param name="target">The object which is to receive the focus.</param>
      /// <returns>A context which can be used to later restore the focus to the object that had it when this was called.</returns>
#if !SILVERLIGHT && !NETFX_CORE
      public static FocusContext SetTemporaryFocus(UIElement target)
#else
      public static FocusContext SetTemporaryFocus(Control target)
#endif
      {
         var mostRecentlyFocusedElement = MostRecentlyFocusedElement;

#if NETFX_CORE
         var worked = target.Focus(FocusState.Programmatic);
#elif !SILVERLIGHT
         var worked = Keyboard.Focus(target) == target;
#else
         var worked = target.Focus();
#endif

         if (trace.Enabled)
            Trace.TraceInformation(string.Format("FocusHelper.SetTemporaryFocus \"{0}\" ({1})", XamlHelper.GetPath(target), worked));

         var context = new FocusContext(contexts, mostRecentlyFocusedElement, target);
         return context;
      }

      public static void Reset()
      {
         if (trace.Enabled)
            Trace.TraceInformation("FocusHelper.Reset clearing any temp focus that may have been in effect.");
         contexts.Clear();
      }

      static WeakReference weakMostRecentlyFocusedElement;

      /// <summary>
      /// Gets the element which currently has the focus or null if no element has the focus
      /// (e.g., the app may not be active or the Silverlight plugin may not have focus).
      /// </summary>
#if !SILVERLIGHT && !NETFX_CORE
      public static UIElement FocusedElement
#else
      public static Control FocusedElement
#endif
      {
         get
         {
#if !SILVERLIGHT && !NETFX_CORE
            var focusedElement = Keyboard.FocusedElement as UIElement;
#else
            var focusedElement = FocusManager.GetFocusedElement() as Control;
#endif
            var temp = GetTempElement(focusedElement);

            if (focusedElement != null && (temp == null || focusedElement != temp))
               weakMostRecentlyFocusedElement = new WeakReference(focusedElement);

            if (trace.Enabled)
               Trace.TraceInformation(string.Format("FocusHelper.FocusedElement is {0}", focusedElement != null ? XamlHelper.GetPath(focusedElement) : "None"));
            return focusedElement; 
         }
      }

#if !SILVERLIGHT && !NETFX_CORE
      public static UIElement MostRecentlyFocusedElement
#else
      public static Control MostRecentlyFocusedElement
#endif
      {
         get
         {
            var focusedElement = FocusedElement;
            var temp = GetTempElement(focusedElement);

            if (focusedElement != null && temp != null && focusedElement == temp)
               focusedElement = null;

            if (focusedElement == null)
#if !SILVERLIGHT && !NETFX_CORE
               focusedElement = weakMostRecentlyFocusedElement != null ? weakMostRecentlyFocusedElement.Target as UIElement: null;
#else
               focusedElement = weakMostRecentlyFocusedElement != null ? weakMostRecentlyFocusedElement.Target as Control : null;
#endif

            if (trace.Enabled)
               Trace.TraceInformation(string.Format("FocusHelper.MostRecentlyFocusedElement is {0}", focusedElement != null ? XamlHelper.GetPath(focusedElement) : "None"));
            return focusedElement;
         }
      }

#if !SILVERLIGHT && !NETFX_CORE
      static UIElement GetTempElement(UIElement focusedElement)
#else
      static Control GetTempElement(Control focusedElement)
#endif
      {
         var removeThese = contexts.Where(context => !context.IsRelevant(focusedElement)).ToList();
         foreach (var removeThis in removeThese)
            contexts.Remove(removeThis);

         return contexts.Any(context => context.IsRelevant(focusedElement)) ? focusedElement : null;
      }
   }
}
